feat(ledger): record what a change was derived from - #261
Conversation
Review — approve, no changesThe question this answers — what was this derived from — is genuinely not answerable from the existing ledger, and it is the one you ask when a generated file is wrong and you need to know which input to fix. Building on the existing ledger rather than adding a provenance store is right for the stated reason: two logs of the same events are two things to keep in step and one place for them to disagree. The four narrowings are what make this trustworthy rather than merely present:
Collected at the single post-dispatch site, same as the ledger append, for the same reason: a per-tool hook is a hook a new tool forgets. That is consistent with the rule AGENTS.md already states, and it is why this stays correct when someone adds a mutating tool. Calling it a derivation record and not a dependency graph, in the docs, is the right disclaimer — they are easy to confuse and only one of them is true here. Two observations, neither a change request:
|
The ledger answers "what changed" and "how do I undo it". It could not answer the third question people actually ask: what was this derived from — which is what you want when a generated file is wrong and you need to know which input to fix, or when something turns up in a diff and you need to know what the turn had open. `derivedFrom` on each record: the files the turn read before making that change. Built on the existing ledger rather than a second store, because a provenance log kept separately from the change log is two things to keep in step and one place for them to disagree. Observed, not declared. These are reads that actually happened, so a tool that ignored its inputs shows nothing rather than a plausible list. Four consequences of that, each of which is a deliberate narrowing: - Only `Read` counts. Grep and Glob take a search *root* and return many paths; calling the root an input claims a derivation the turn did not make, and listing every hit drowns the real inputs in whatever the search swept up. Narrow and true beats wide and approximate. - A failed read is not an input. It gave the turn nothing, and crediting it sends someone to fix a file that was never opened. - The file being written is excluded. Edit reads its own target by construction, so including it would make every edit look self-derived. - Absent, not empty, when there is nothing to say. A field that is always present is a field that stops being read. Collected at the one place every completed tool call passes through — the same site the ledger append already lives at, for the same reason: a per-tool hook is a hook a new tool forgets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4b31886 to
d66df0c
Compare
Closes the last 未做 item in
docs/FLOATBOAT_ADOPTION_PLAN.md§4.1: "制品provenance… 未做" (Selfware §11.1).The question the ledger could not answer
It answers "what changed" and "how do I undo it". Not: what was this derived from — which is what you ask when a generated file is wrong and you need to know which input to fix, or when something turns up in a diff and you need to know what the turn had open.
Built on the existing ledger, not a second store — a provenance log kept separately from the change log is two things to keep in step and one place for them to disagree.
Observed, not declared
These are reads that actually happened, so a tool that ignored its inputs shows nothing rather than a plausible list. Four deliberate narrowings, each tested:
ReadcountsGrep/Globtake a search root and return many paths. Calling the root an input claims a derivation the turn did not make; listing every hit drowns the real inputs in whatever the search swept up. Narrow and true beats wide and approximate.Editreads its own target by construction — including it makes every edit look self-derived.Collected at the one place every completed tool call passes through — the same site the ledger append already lives at, for the same reason: a per-tool hook is a hook a new tool forgets.
It is a derivation record, not a dependency graph: what this one turn read, not what the file transitively depends on. The docs say so, because the two are easy to confuse and only one of them is true here.
Verification
typecheck, lint, format, docs clean; full suite green. 10 new tests, including two end-to-end through a real
runAgentrun — provenance is recorded from an actualRead→Writesequence, and a failed read is not credited.🤖 Generated with Claude Code